CloudFront+S3とCodePipelineをCFnでデプロイする – その1
はじめに
静的サイトの公開でよく利用されるCloudFront+S3、Githubにソースコードがある構成でCodepipelineでデプロイできるCFnを書いてみました。
構成図
AWSまたは他のドメインレジストラで取得したドメインのパブリックホステッドゾーンがあることが前提となります。
また、設計方針としてCloudFrontからのみAmazon S3 コンテンツへのアクセスを制限します。OAIを設定する為、CloudFrontとS3でテンプレートを分割すると面倒な分割(OAI作成→S3バケット作成→CloudFront作成)になってしまう為、CloudFront+S3テンプレートは一つのテンプレートで作成します。CloudFrontののSSL Certificateに適用するACMテンプレート、CloudFrontのエイリアスレコードを作成するRoute53テンプレート、GitHubからソースコードをデプロイするCodePipelineテンプレートを作成します。
ACMテンプレートの作成と実行
ACMのDNS検証は、2020年6月にアップデートで対応したのでリンクを参考にACMテンプレートを作成します。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
SystemName:
Type: String
Default: example
BucketName:
Type: String
Default: www.example.xxx
HostedZone:
Type: String
Default: ZXXXXXXXXXXXXXXXXXXXX
Resources:
ACM:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Ref BucketName
DomainValidationOptions:
- DomainName: !Ref BucketName
HostedZoneId: !Ref HostedZone
ValidationMethod: DNS
Tags:
-
Key: Name
Value: !Sub ${SystemName}-${BucketName}-acm
Outputs:
ACMCertificateARN:
Value: !Ref ACM
Export:
Name: !Sub ${SystemName}-CertificateARN
また、証明書リソースはCloudFrontがデプロイされるus-east-1リージョンに作成する必要があります。NestedStack且つOutputsセクションは同一リージョン以外で作成、参照ができない為、ACMテンプレートをus-east-1でデプロイします。
デプロイ完了後、Outputされた証明書リソースのARNをメモします。
CloudFront+S3テンプレートの作成(子スタック)
次にCloudFront+S3テンプレートを作成します。DependsOn属性でOAI作成後にバケットポリシーとCloudFrontを作成するように設定します。また、Route53でエイリアスレコードを追加する為、Aliases
プロパティでCNAMEを定義します。Parametersは、親Stackで設定するのでデータ型のTypeプロパティだけ設定します。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
SystemName:
Type: String
BucketName:
Type: String
HostedZone:
Type: String
MinimumProtocolVersion:
Type: String
SslSupportMethod:
Type: String
CertificateARN:
Type: String
Resources:
S3bucketForOrigin:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${SystemName}-${BucketName}-${AWS::AccountId}
CorsConfiguration:
CorsRules:
- AllowedHeaders:
- Authorization
- Content-Length
AllowedMethods:
- GET
AllowedOrigins:
- '*'
MaxAge: 3000
S3BucketPolicy:
Type: 'AWS::S3::BucketPolicy'
DependsOn: OAI
Properties:
PolicyDocument:
Statement:
- Sid: APIReadForGetBucketObjects
Effect: Allow
Principal:
CanonicalUser: !GetAtt
- OAI
- S3CanonicalUserId
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref S3bucketForOrigin
- /*
Bucket: !Ref S3bucketForOrigin
OAI:
Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity'
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: OAIConfig
CloudFrontDistribution:
Type: 'AWS::CloudFront::Distribution'
DependsOn:
- S3bucketForOrigin
- OAI
Properties:
DistributionConfig:
Aliases:
- !Sub ${BucketName}
HttpVersion: http2
Origins:
- DomainName: !GetAtt
- S3bucketForOrigin
- DomainName
Id: !Sub S3bucketForOrigin
S3OriginConfig:
OriginAccessIdentity: !Join
- ''
- - origin-access-identity/cloudfront/
- !Ref OAI
Enabled: 'true'
DefaultCacheBehavior:
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
TargetOriginId: S3bucketForOrigin
ForwardedValues:
QueryString: 'false'
ViewerProtocolPolicy: redirect-to-https
DefaultTTL: 86400
MaxTTL: 31536000
MinTTL: 60
Compress: true
DefaultRootObject: index.html
CustomErrorResponses:
- ErrorCachingMinTTL: 300
ErrorCode: 400
ResponseCode: 200
ResponsePagePath: /
- ErrorCachingMinTTL: 300
ErrorCode: 403
ResponseCode: 200
ResponsePagePath: /
- ErrorCachingMinTTL: 300
ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /
ViewerCertificate:
AcmCertificateArn: !Sub ${CertificateARN}
MinimumProtocolVersion: !Sub ${MinimumProtocolVersion}
SslSupportMethod: !Sub ${SslSupportMethod}
Outputs:
S3bucketForOrigin:
Value: !Ref S3bucketForOrigin
S3BucketSecureURL:
Value: !Join
- ''
- - 'https://'
- !GetAtt
- S3bucketForOrigin
- DomainName
CloudFrontDistributionID:
Value: !Ref CloudFrontDistribution
Export:
Name: !Sub ${SystemName}-CloudFrontDistributionID
CloudFrontDomainName:
Value: !GetAtt
- CloudFrontDistribution
- DomainName
Export:
Name: !Sub ${SystemName}-CloudFrontDomainName
CloudFrontSecureURL:
Value: !Join
- ''
- - 'https://'
- !GetAtt
- CloudFrontDistribution
- DomainName
OAI:
Value: !Ref OAI
Route53テンプレートの作成(子スタック)
ユーザガイドを参考にRoute53テンプレートを作成します。先ほどのテンプレートと同じくParametersは、親Stackで設定するのでデータ型のTypeプロパティだけ設定します。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
SystemName:
Type: String
BucketName:
Type: String
HostedZone:
Type: String
Resources:
CloudFrontDNSRecord:
Type: 'AWS::Route53::RecordSetGroup'
Properties:
HostedZoneId: !Ref HostedZone
RecordSets:
- Name: !Ref BucketName
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: {'Fn::ImportValue': !Sub '${SystemName}-CloudFrontDomainName'}
Outputs:
URL:
Value: !Sub 'https://${BucketName}'
実はユーザガイドにたどり着くまで、AliasTargetのHostedZoneIdプロパティの値を誤って自身の管理するHostedZoneのIDを指定したことでハマってしまいました。。以下の通り、CloudFront.netのホストゾーンIDを指定する必要があります。
注記エイリアスリソースレコードセットを作成するときは、以下の例に示すように
Z2FDTNDATAQYW2
プロパティにHostedZoneId
を指定する必要があります。CloudFront 用のエイリアスリソースレコードセットは、プライベートホストゾーンでは作成できません。
Pipeline以外のテンプレートが揃いました。静的サイト構成として動作に問題ないか確認する為、ネストした親テンプレートを作成します。
NestedStackテンプレートの作成(親スタック)
子スタックのテンプレートは、S3に格納する必要があるので任意のS3バケットに格納してTemplateURL
をオブジェクトURLに置換してください。ついでに親スタックのテンプレートもS3にアップロードします。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
SystemName:
Type: String
Default: example
BucketName:
Type: String
Default: www.example.xxx
HostedZone:
Type: String
Default: Z10293303OK6IN7V0JZVB
MinimumProtocolVersion:
Type: String
Default: TLSv1.2_2019
AllowedValues:
- SSLv3
- TLSv1
- TLSv1.1_2016
- TLSv1.2_2018
- TLSv1.2_2019
- TLSv1_2016
SslSupportMethod:
Type: String
Default: sni-only
AllowedValues:
- sni-only
- static-ip
- vip
CertificateARNatVirginia:
Type: String
Default: ''
Resources:
HOSTING:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/hosting.yml'
Parameters:
SystemName: !Sub ${SystemName}
BucketName: !Sub ${BucketName}
HostedZone: !Sub ${HostedZone}
MinimumProtocolVersion: !Sub ${MinimumProtocolVersion}
SslSupportMethod: !Sub ${SslSupportMethod}
CertificateARN: !Sub ${CertificateARN}
DNS:
Type: AWS::CloudFormation::Stack
DependsOn: HOSTING
Properties:
TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/dnsrecord.yml'
Parameters:
SystemName: !Sub ${SystemName}
BucketName: !Sub ${BucketName}
HostedZone: !Sub ${HostedZone}
NestedStackの実行
それでは実行してきます。Amazon S3 URLに親スタックテンプレートのオブジェクトURLを入力します。
スタックの名前、CertificateARNに証明書リソースのARNを入力します。以降、デフォルトで進んでStackを実行します。
ステータスがCREATE_COMPLETEされたことを確認します。 OriginのS3バケットは、hosting-HOSTING-xxxxxxxxの出力タブ、URLはhosting-DNS-xxxxxxxxxの出力タブで確認きます。
S3バケットに任意のhtmlファイルを配置して、URLにアクセスできるようになります。
もし、Access Deniedや307がでた場合、以下の理由でエラーになっている可能性があります。
Amazon S3 から HTTP 307 Temporary Redirect レスポンスが返るのはなぜですか?
回避する為には、CloudFrontのOrigin Domain Nameにリージョンエンドポイントを追記することでエラーを回避できます。
Origin Domain Name変更前:バケット名.s3.amazonaws.com
Origin Domain Name変更後:バケット名.s3-ap-northeast-1.amazonaws.com
ここまでで静的サイトの動作確認ができました。 次回、CodePipelineテンプレートで静的サイトを更新する記事を書きたいと思います。